
package column

import (
	"github.com/treelab/updatecolumn/models"
)

  func convertColumn(){

  }

  async defaultConvert(
    convertColumn: ConvertColumn_v2,
    o: CommonOptions,
    helper: { rows?: Row[]; columnCopy?: Column; referenceColumns?: Column[]; userGroup?: any },
    updateColumnHelper?: UpdateColumnHelper,
  ) {
    const { command } = convertColumn;
    const { column } = command;

    this.instanceFactory.getColumnInstance(column.columnType).deleteColumnEdges(command, o);
    await this.updateColumn(command, o, helper, updateColumnHelper);
    return this;
  }

  protected createColumn(command: CreateColumn, options: CommonOptions, views: View[]): Column {
    const { columnConfig, workspaceId, coreId, tableId, _metadata } = command;
    const column = options.ormTransaction.nodeFor(Column);
    const columnId = column.uid!;
    column.isDefault = command.isDefault ?? false;
    column.name = columnConfig.name!;
    column.type = GraphNodeTypes.COLUMN;
    column.columnType = command.columnConfig.type;

    const table = options.ormTransaction.nodeFor(Table, { uid: command.tableId });
    column.fromTable.add(table);
    table.hasColumn.add(column);

    this.updateColumn({ column, ...command }, options, { columnCopy: column }, { columnId, isSymmetric: true });
    // add column to view
    const typeOptions = JSON.parse(JSON.stringify(columnConfig));
    // @ts-ignore @TODO columnID 取不到
    views.forEach(view => {
      const columns = view.hasColumn.get();
      const maxOrderColumn = _.maxBy(columns, 'order')!;
      // get Column name
      columnConfig.name = getUniqueName(columns, columnConfig.name!);
      const orderFacet = view.hasColumn.getFacet(maxOrderColumn);
      const order = (orderFacet?.order ?? maxOrderColumn.order ?? columns.length) + 1;
      const addColumnToView = plainToClass(AddColumnToView, {
        columnId,
        coreId,
        tableId,
        workspaceId,
        viewId: view.uid!,
        order: order!,
        width: COLUMN_DEFAULT_WIDTH,
        name: columnConfig.name!,
        type: columnConfig.type,
        typeOptions,
        _metadata,
      });
      this.addColumnToView(
        addColumnToView,
        options.ormTransaction.nodeFor(View, { uid: view.uid }),
        column,
        new OrderFacet({ order, width: COLUMN_DEFAULT_WIDTH, isHidden: false }),
        options,
      );
    });
    return column;
  }

  protected addColumnToView(
    command: AddColumnToView,
    view: View,
    column: Column,
    orderFacet = new OrderFacet({
      order:
        view.hasColumn.getFacet(view.hasColumn.get()[view.hasColumn.get().length - 1])?.order ??
        view.hasColumn.get().length,
      width: COLUMN_DEFAULT_WIDTH,
      isHidden: false,
    }),
    o: CommonOptions,
  ) {
    const { workspaceId, coreId, tableId, columnId, name, type, typeOptions, _metadata } = command;
    view.hasColumn.withFacet(orderFacet).add(column);
    column.fromView.withFacet(orderFacet).add(view);
    // @TODO √ ColumnId 没有临时 ID
    o.events.addEvents(
      new ColumnAddedToView(
        workspaceId,
        coreId,
        tableId,
        view.uid!,
        columnId!,
        orderFacet.order!,
        orderFacet.width!,
        name,
        type,
        typeOptions,
        _metadata,
      ),
      ['columnId'],
    );
  }

  async updateColumn(
    command: UpdateColumn_v2,
    o: CommonOptions,
    helper: { rows?: Row[]; columnCopy?: Column; referenceColumns?: Column[]; userGroup?: any },
    updateColumnHelper: UpdateColumnHelper = {},
  ) {
    const { columnConfig, column } = command;
    const fromColumn = helper.columnCopy!;
    column.columnType = columnConfig.type;
    const { isSymmetric = false } = updateColumnHelper;

    this.addColumnUpdateEvent(command, o, updateColumnHelper);

    if (!isSymmetric && this.needConvertSymmetricColumn(fromColumn, columnConfig)) {
      const symmectricColumn = column.hasSymmetricColumn.get()[0];
      const columnCopy = _.cloneDeep(symmectricColumn);
      const covertTextCommand = {
        workspaceId: column.foreignWorkspaceId,
        coreId: column.foreignCoreId,
        tableId: column.foreignTableId,
        column: symmectricColumn,
        columnConfig: {
          type: ColumnType.TEXT,
          name: symmectricColumn.name,
        },
        _metadata: { ...command._metadata, source: Source.SAGA },
      };

      await this.instanceFactory
        .getColumnInstance(ColumnType.TEXT)
        .convertColumn(covertTextCommand, o, helper, { isSymmetric: true });
      const cellTransaction = this.instanceFactory.getCellInstance(ColumnType.TEXT);
      await cellTransaction.convertCell({ ...covertTextCommand, column: symmectricColumn }, o, {
        columnCopy,
        rows: helper.rows,
      });
      // update watcher
      cellTransaction.updateDependentFormulaCell(
        { command: covertTextCommand, column: symmectricColumn, status: UpdateCellStatus.UPDATING_COLUMN },
        o,
      );
    }

    if (!isSymmetric && this.needTimelineUpdate(fromColumn, columnConfig)) {
      const timelineViews = column.hasTimelineView.get();
      if (timelineViews.length) {
        if (column.columnType === ColumnType.RECORD_REFERENCE) {
          this.reportTimelineError(command, o, timelineViews, { parentRowColumnId: column.uid });
        } else if (column.columnType === ColumnType.DATETIME) {
          this.reportTimelineError(command, o, timelineViews, { dateColumnId: column.uid });
        }
      }
    }

    if (!isSymmetric && this.needKanbanUpdate(fromColumn, columnConfig)) {
      const kanbanViews = column.hasKanbanView.get();
      // Removed all the stackedby relationships from
      if (kanbanViews.length) {
        this.reportKanbanError(command, o, kanbanViews);
      }
    }

    if (this.needConvertSymmetricColumn(column, command.columnConfig)) {
      this.reportLookupOrRollupError(
        { ...command, _metadata: { ...command._metadata, source: Source.SAGA } },
        o,
        helper,
        column,
      );
    }

    const needRemovedControls = this.needRemovedControls(command);
    if (needRemovedControls.length) {
      this.removeControlValues(command, o, needRemovedControls);
    }

    this.addUpdateLookupColumnConfigEvents(command, o);

    return this;
  }

  protected addColumnUpdateEvent(
    command: UpdateColumn_v2,
    o: CommonOptions,
    updateColumnHelper: UpdateColumnHelper = {},
  ) {
    const { workspaceId, coreId, tableId, columnConfig, column, _metadata } = command;
    column.columnType = columnConfig.type;
    const { columnId } = updateColumnHelper;
    o.events.addEvents(
      new ColumnConfigUpdated(workspaceId, coreId, tableId, column.uid ?? columnId!, columnConfig, _metadata),
      column.uid ? ['columnId'] : [],
    );
  }

  private needConvertSymmetricColumn(column: Column, toColumnConfig: ConfigTypes) {
    if (column.hasSymmetricColumn.get().length) {
      if (toColumnConfig.type !== ColumnType.RECORD_REFERENCE) {
        return true;
      }
      if (toColumnConfig.type === ColumnType.RECORD_REFERENCE) {
        const _toColumnConfig = toColumnConfig as RecordReferenceColumnConfig;
        if (
          column.foreignWorkspaceId !== _toColumnConfig.foreignWorkspaceId ||
          column.foreignCoreId !== _toColumnConfig.foreignCoreId ||
          column.foreignTableId !== _toColumnConfig.foreignTableId
        ) {
          return true;
        }
      }
    }
    return false;
  }

  private needTimelineUpdate(fromColumn: Column, toColumnConfig: ConfigTypes) {
    if (fromColumn.type === ColumnType.RECORD_REFERENCE) {
      if (toColumnConfig.type !== ColumnType.RECORD_REFERENCE) {
        return true;
      }
      const _toColumnConfig = toColumnConfig as RecordReferenceColumnConfig;
      if (
        fromColumn.foreignWorkspaceId !== _toColumnConfig.foreignWorkspaceId ||
        fromColumn.foreignCoreId !== _toColumnConfig.foreignCoreId ||
        fromColumn.foreignTableId !== _toColumnConfig.foreignTableId
      ) {
        return true;
      }
    } else if (fromColumn.type === ColumnType.DATETIME) {
      if (toColumnConfig.type !== ColumnType.DATETIME) {
        return true;
      }
    }
    return false;
  }

  private needKanbanUpdate(fromColumn: Column, toColumnConfig: ConfigTypes) {
    if (fromColumn.type === ColumnType.SELECT) {
      if (toColumnConfig.type !== ColumnType.SELECT) {
        return true;
      }
    }
    return false;
  }

  public needRemovedControls(
    command: UpdateColumn_v2, // | RemoveColumn | UpdateView
  ): Array<FilterValue | SortValue | GroupValue | AggregateValue> {
    /* If the column that's being updated is the same type or same filter type
    keep the same filter. */
    const { column } = command;
    const fileterValues = column.hasFilterValue.get();
    const needRemovedControls: Array<FilterValue | SortValue | GroupValue | AggregateValue> = [];
    if (fileterValues.length) {
      if (command instanceof UpdateColumn_v2) {
        const { type: columnType } = command.columnConfig;
        fileterValues.forEach((existingFilterValue: FilterValue) => {
          const { filterType } = existingFilterValue;
          switch (filterType) {
            case FilterType.TEXT: {
              const isValidFilterTextFilterType =
                columnType === ColumnType.TEXT ||
                columnType === ColumnType.LONG_TEXT ||
                columnType === ColumnType.UNIQUE_ID;
              if (!isValidFilterTextFilterType) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
            case FilterType.NUMBER: {
              if (columnType !== ColumnType.NUMBER) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
            case FilterType.ATTACHMENT: {
              if (columnType !== ColumnType.MULTI_ATTACHMENT) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
            case FilterType.CHECKBOX: {
              if (columnType !== ColumnType.CHECKBOX) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
            case FilterType.DATE: {
              if (columnType !== ColumnType.DATETIME) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
            case FilterType.MULTI_SELECT: {
              if (columnType !== ColumnType.MULTI_SELECT) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
            case FilterType.SELECT: {
              if (columnType !== ColumnType.SELECT) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
            case FilterType.RECORD_REFERENCE: {
              if (columnType !== ColumnType.RECORD_REFERENCE) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
            case FilterType.LOOKUP: {
              if (columnType !== ColumnType.LOOKUP) {
                needRemovedControls.push(existingFilterValue);
              }
              break;
            }
          }
        });
      } else {
        needRemovedControls.push(...fileterValues);
      }
    }
    const groupValues = column.hasGroupValue.get();
    if (groupValues.length) {
      needRemovedControls.push(...groupValues);
    }
    const sortValues = column.hasSortValue.get();
    if (sortValues.length) {
      needRemovedControls.push(...sortValues);
    }
    const aggregateValues = column.hasAggregateValue.get();
    if (aggregateValues.length) {
      // delete aggregate according column type
      _.forEach(aggregateValues, aggregateValue => {
        const { type: columnType } = command.columnConfig;
        try {
          validateAggregateColumnType(columnType, aggregateValue.aggregateFunction as AggregateFunctions);
        } catch (e) {
          needRemovedControls.push(aggregateValue);
        }
      });
    }
    return needRemovedControls;
  }

  private addUpdateLookupColumnConfigEvents(command: UpdateColumn_v2, o: CommonOptions, typeOptions?: any) {
    const { column, columnConfig, _metadata } = command;
    const lookupColumns = column.hasLookupUpdate.get();

    _.forEach(lookupColumns, lookupColumn => {
      const table = lookupColumn.fromTable.get()[0];
      const core = table.fromCore.get()[0];
      const workspace = core.fromWorkspace.get()[0];
      if (!typeOptions) {
        const columnOptions = this.getColumnTypeOptions(lookupColumn, columnConfig);
        typeOptions = JSON.parse(columnOptions.typeOptions);
      }
      o.events.addEvents(
        new LookupColumnConfigUpdated(workspace.uid!, core.uid!, table.uid!, lookupColumn.uid!, typeOptions, _metadata),
      );
      // @TODO 如果用 plainToClass 会造成未知的堆栈溢出, 有空再看
      this.addUpdateLookupColumnConfigEvents(
        // plainToClass(UpdateColumn_v2, {
        //   workspaceId: workspace.uid!,
        //   coreId: core.uid!,
        //   tableId: table.uid!,
        //   column: lookupColumn,
        //   columnConfig: {
        //     type: lookupColumn.columnType,
        //     name: lookupColumn.name,
        //     recordReferenceColumnId: lookupColumn.recordReferenceColumnId,
        //     foreignLookupColumnId: lookupColumn.foreignLookupColumnId,
        //   },
        //   _metadata,
        // }),
        {
          workspaceId: workspace.uid!,
          coreId: core.uid!,
          tableId: table.uid!,
          column: lookupColumn,
          columnConfig: {
            type: lookupColumn.columnType,
            name: lookupColumn.name,
            recordReferenceColumnId: lookupColumn.recordReferenceColumnId,
            foreignLookupColumnId: lookupColumn.foreignLookupColumnId,
          },
          _metadata,
        },
        o,
        typeOptions,
      );
    });
    return this;
  }

  public getColumnTypeOptions(
    column: Column,
    columnConfig?: any,
  ): {
    name: any;
    type: any;
    typeOptions: string;
  } {
    // If a column is not provided, then get the column by its ID.
    if (columnConfig) {
      const { name: columnName, type: _columnType } = columnConfig;
      // Here we are formatting the prop to match what we have for other lookup config options.
      const capitalize = (s: string) => s.charAt(0).toUpperCase() + s.slice(1);
      const newColumnConfig: any = {};
      Object.keys(columnConfig).forEach((prop: string) => {
        if (prop === 'name') {
          newColumnConfig.name = columnConfig[prop];
        } else if (prop === 'type') {
          newColumnConfig.lookupColumnType = columnConfig[prop];
        } else {
          newColumnConfig[`lookup${capitalize(prop)}`] = columnConfig[prop];
        }
      });
      return { name: columnName, type: _columnType, typeOptions: JSON.stringify(newColumnConfig) };
    }
    const {
      name,
      type: columnType,
      precision,
      defaultNumber,
      idFormat,
      idPrefix,
      foreignWorkspaceId,
      numberFormat,
      formula,
      foreignCoreId,
      foreignTableId,
      foreignLookupColumnId,
      oneToOne,
      recordReferenceColumnId,
      symmetricColumnId,
      useGMT,
      useNotify,
      useGroup,
      useMultiple,
      timeFormat,
      includeTime,
      dateFormat,
      ratingStyle,
      currencyFormat,
      ratingMax,
    } = column;
    const typeOptions = JSON.stringify({
      precision,
      defaultNumber,
      idFormat,
      idPrefix,
      foreignWorkspaceId,
      foreignCoreId,
      foreignTableId,
      foreignLookupColumnId,
      recordReferenceColumnId,
      oneToOne,
      symmetricColumnId,
      formula,
      numberFormat,
      useGMT,
      useNotify,
      useGroup,
      useMultiple,
      timeFormat,
      includeTime,
      dateFormat,
      currencyFormat,
      ratingStyle,
      ratingMax,
    });
    return { name, type: columnType, typeOptions };
  }

  private async reportTimelineError(
    command: UpdateColumn_v2, // | RemoveColumn,
    o: CommonOptions,
    timelineViews?: View[] | readonly View[],
    options?: {
      dateColumnId?: string;
      parentRowColumnId?: string;
    },
  ) {
    const viewOptionsRemoved: any[] = [];
    // Removed all the stackedby relationships from
    if (!timelineViews) {
      return viewOptionsRemoved;
    }
    _.forEach(timelineViews, timelineView => {
      this.removeTimelineRelations(
        timelineView.uid!,
        o,
        options ? options.dateColumnId : undefined,
        options ? options.parentRowColumnId : undefined,
      );
      const viewOptionRemoved = new ViewOptionRemoved(
        command.workspaceId,
        command.coreId,
        command.tableId,
        timelineView.uid!,
        command._metadata,
      );
      o.events.addEvents(viewOptionRemoved);
    });
    return this;
  }

  private removeTimelineRelations(viewId: string, o: CommonOptions, dateColumnId?: string, parentRowColumnId?: string) {
    if (dateColumnId) {
      o.delQuads.push(...this.dgraphHelperService.getRemoveEdgeQuads(dateColumnId, Relation.HAS_TIMELINE_VIEW));
      o.delQuads.push(...this.dgraphHelperService.getRemoveEdgeQuads(viewId, Relation.HAS_START_DATE_COLUMN));
      o.delQuads.push(...this.dgraphHelperService.getRemoveEdgeQuads(viewId, Relation.HAS_END_DATE_COLUMN));
    }
    if (parentRowColumnId) {
      o.delQuads.push(...this.dgraphHelperService.getRemoveEdgeQuads(parentRowColumnId, Relation.HAS_TIMELINE_VIEW));
      o.delQuads.push(...this.dgraphHelperService.getRemoveEdgeQuads(viewId, Relation.HAS_PARENT_ROW_COLUMN));
    }
  }

  private async reportKanbanError(
    command: UpdateColumn_v2, // | RemoveColumn,
    o: CommonOptions,
    kanbanViews?: View[] | readonly View[],
  ) {
    const viewOptionsRemoved: any[] = [];
    // Removed all the stackedby relationships from
    if (!kanbanViews) {
      return viewOptionsRemoved;
    }
    this.removeStackedByColumn(command.column, o, kanbanViews);
    kanbanViews.forEach((kanbanView: any) =>
      o.events.addEvents(
        new ViewOptionRemoved(command.workspaceId, command.coreId, command.tableId, kanbanView.id, command._metadata),
      ),
    );
    return viewOptionsRemoved;
  }

  private removeStackedByColumn(column: Column, o: CommonOptions, views: View[] | readonly View[]) {
    const columnId = column.uid!;
    _.forEach(views, view => {
      const lanes = view.hasLane.get();
      _.forEach(lanes, lane => o.delQuads.push(...this.dgraphHelperService.getRemoveObjectQuads(lane.uid!)));
      o.delQuads.push(...this.dgraphHelperService.getRemoveEdgeQuads(view.uid!, Relation.HAS_STACKED_COLUMN, columnId));
      o.delQuads.push(...this.dgraphHelperService.getRemoveEdgeQuads(view.uid!, Relation.HAS_LANE));
    });
  }

  public removeControlValues(
    command: UpdateColumn_v2, // | RemoveColumn | RemoveControlValue | UpdateView,
    o: CommonOptions,
    controlValues: Array<FilterValue | SortValue | GroupValue | AggregateValue>,
  ) {
    const { workspaceId, coreId, tableId, _metadata } = command;
    controlValues.map(controlValue => {
      const column = controlValue.fromColumn.get();
      o.delQuads.push(...this.dgraphHelperService.getRemoveObjectQuads(controlValue.uid!));
      if (controlValue.type.includes(GraphNodeTypes.FILTER_VALUE)) {
        const filter = (controlValue as FilterValue).fromFilter.get();
        if (column[0]?.uid) {
          o.delQuads.push(
            ...this.dgraphHelperService.getRemoveEdgeQuads(column[0].uid!, Relation.HAS_FILTER_VALUE, controlValue.uid),
          );
        }
        if (filter[0]) {
          o.delQuads.push(
            ...this.dgraphHelperService.getRemoveEdgeQuads(filter[0].uid!, Relation.HAS_FILTER_VALUE, controlValue.uid),
          );
          const filterViewId = filter[0]?.fromView.get()[0]?.uid!;
          o.events.addEvents(
            new ControlValueRemoved(
              workspaceId,
              coreId,
              tableId,
              filterViewId,
              ControlType.FILTER_VALUE,
              controlValue.uid!,
              _metadata,
            ),
          );
        }
      } else if (controlValue.type.includes(GraphNodeTypes.GROUP_VALUE)) {
        const group = (controlValue as GroupValue).fromGroup.get();
        if (column[0]?.uid) {
          o.delQuads.push(
            ...this.dgraphHelperService.getRemoveEdgeQuads(column[0]?.uid!, Relation.HAS_GROUP_VALUE, controlValue.uid),
          );
        }
        if (group[0]) {
          o.delQuads.push(
            ...this.dgraphHelperService.getRemoveEdgeQuads(group[0]?.uid!, Relation.HAS_GROUP_VALUE, controlValue.uid),
          );
        }
        const filterGroupViewId = group[0]?.fromView?.get()[0]?.uid!;
        if (filterGroupViewId) {
          o.events.addEvents(
            new ControlValueRemoved(
              workspaceId,
              coreId,
              tableId,
              filterGroupViewId,
              ControlType.GROUP_VALUE,
              controlValue.uid!,
              _metadata,
            ),
          );
        }
      } else if (controlValue.type.includes(GraphNodeTypes.SORT_VALUE)) {
        const sort = (controlValue as SortValue).fromSort.get();
        if (column[0]?.uid) {
          o.delQuads.push(
            ...this.dgraphHelperService.getRemoveEdgeQuads(column[0]?.uid!, Relation.HAS_SORT_VALUE, controlValue.uid),
          );
        }
        if (sort[0]) {
          o.delQuads.push(
            ...this.dgraphHelperService.getRemoveEdgeQuads(sort[0]?.uid!, Relation.HAS_SORT_VALUE, controlValue.uid),
          );
        }
        const filterViewGroupId = sort[0]?.fromView?.get()[0]?.uid!;
        if (filterViewGroupId) {
          o.events.addEvents(
            new ControlValueRemoved(
              workspaceId,
              coreId,
              tableId,
              filterViewGroupId,
              ControlType.SORT_VALUE,
              controlValue.uid!,
              _metadata,
            ),
          );
        }
      } else if (controlValue.type.includes(GraphNodeTypes.AGGREGATE_VALUE)) {
        const aggregate = (controlValue as AggregateValue).fromAggregate.get();
        if (column[0]?.uid) {
          o.delQuads.push(
            ...this.dgraphHelperService.getRemoveEdgeQuads(
              column[0]?.uid!,
              Relation.HAS_AGGREGATE_VALUE,
              controlValue.uid,
            ),
          );
        }
        if (aggregate[0]) {
          o.delQuads.push(
            ...this.dgraphHelperService.getRemoveEdgeQuads(
              aggregate[0]?.uid!,
              Relation.HAS_AGGREGATE_VALUE,
              controlValue.uid,
            ),
          );
        }
        const aggregateViewId = aggregate[0]?.fromView?.get()[0]?.uid!;
        if (aggregateViewId) {
          o.events.addEvents(
            new ControlValueRemoved(
              workspaceId,
              coreId,
              tableId,
              aggregateViewId,
              ControlType.AGGREGATE_VALUE,
              controlValue.uid!,
              _metadata,
            ),
          );
        }
      }
    });
  }

  private reportLookupOrRollupError(
    command: UpdateColumn_v2, // | RemoveColumn,
    o: CommonOptions,
    helper: { rows?: Row[]; columnCopy?: Column; referenceColumns?: Column[]; userGroup?: any },
    column?: Column,
    removedColumns: string[] = [],
  ) {
    column = column || command.column;
    let lookupColumns = column.hasLookupColumn.get();
    if (lookupColumns.length) {
      lookupColumns = lookupColumns.filter(col => !removedColumns.includes(col.uid!));
      _.forEach(lookupColumns, lookupColumn => {
        const _command = plainToClass(UpdateColumn_v2, {
          ...command,
          // @ts-ignore
          column: null,
          columnConfig: {
            type: lookupColumn.columnType,
            name: lookupColumn.name,
            recordReferenceColumnId: lookupColumn.recordReferenceColumnId,
            foreignLookupColumnId: lookupColumn.foreignLookupColumnId,
            errorCode: ColumnErrorCodes.LOOKUP_COLUMN_REMOVED,
          },
        });
        _command.column = lookupColumn;
        this.instanceFactory
          .getColumnInstance(lookupColumn.columnType)
          .updateColumn(_command, o, { columnCopy: lookupColumn });
        this.reportLookupOrRollupError(_command, o, helper, lookupColumn);
      });
    }
    let lookupUpdatedColumns = column.hasLookupUpdate.get();
    lookupUpdatedColumns = lookupUpdatedColumns.filter(col => !removedColumns.includes(col.uid!));
    _.forEach(lookupUpdatedColumns, lookupUpdatedColumn => {
      const table = lookupUpdatedColumn.fromTable.get()[0];
      const core = table.fromCore.get()[0];
      const workspace = core.fromWorkspace.get()[0];
      const _command = plainToClass(UpdateColumn_v2, {
        workspaceId: workspace.uid!,
        coreId: core.uid!,
        tableId: table.uid!,
        // @ts-ignore
        column: null,
        columnConfig: {
          type: lookupUpdatedColumn.columnType,
          name: lookupUpdatedColumn.name,
          recordReferenceColumnId: lookupUpdatedColumn.recordReferenceColumnId,
          foreignLookupColumnId: lookupUpdatedColumn.foreignLookupColumnId,
          errorCode: ColumnErrorCodes.LOOKUP_COLUMN_REMOVED,
        },
        _metadata: command._metadata,
      });
      _command.column = lookupUpdatedColumn;
      this.instanceFactory
        .getColumnInstance(lookupUpdatedColumn.columnType)
        .updateColumn(_command, o, { columnCopy: lookupUpdatedColumn });
      this.reportLookupOrRollupError(_command, o, helper, lookupUpdatedColumn);
    });

    let rollupColumns = column.hasRollupColumn.get();
    if (rollupColumns.length) {
      rollupColumns = rollupColumns.filter(col => !removedColumns.includes(col.uid!));
      _.forEach(rollupColumns, rollupColumn => {
        const _command = plainToClass(UpdateColumn_v2, {
          ...command,
          // @ts-ignore
          column: null,
          columnConfig: {
            type: rollupColumn.columnType,
            name: rollupColumn.name,
            formula: rollupColumn.formula,
            recordReferenceColumnId: rollupColumn.recordReferenceColumnId,
            errorCode: ColumnErrorCodes.ROLLUP_COLUMN_REMOVED,
          },
        });
        _command.column = rollupColumn;
        this.instanceFactory
          .getColumnInstance(rollupColumn.columnType)
          .updateColumn(_command, o, { columnCopy: rollupColumn });
        this.reportLookupOrRollupError(_command, o, helper, rollupColumn);
      });
    }

    let rollupUpdatedColumns = column.hasRollupUpdate.get();
    rollupUpdatedColumns = rollupUpdatedColumns.filter(col => !removedColumns.includes(col.uid!));
    _.forEach(rollupUpdatedColumns, rollupUpdatedColumn => {
      const table = rollupUpdatedColumn.fromTable.get()[0];
      const core = table.fromCore.get()[0];
      const workspace = core.fromWorkspace.get()[0];
      const _command = plainToClass(UpdateColumn_v2, {
        workspaceId: workspace.uid!,
        coreId: core.uid!,
        tableId: table.uid!,
        // @ts-ignore
        column: null,
        columnConfig: {
          type: rollupUpdatedColumn.columnType,
          name: rollupUpdatedColumn.name,
          formula: rollupUpdatedColumn.formula,
          recordReferenceColumnId: rollupUpdatedColumn.recordReferenceColumnId,
          errorCode: ColumnErrorCodes.ROLLUP_COLUMN_REMOVED,
        },
        _metadata: command._metadata,
      });
      _command.column = rollupUpdatedColumn;
      this.instanceFactory
        .getColumnInstance(rollupUpdatedColumn.columnType)
        .updateColumn(_command, o, { columnCopy: rollupUpdatedColumn });
      this.reportLookupOrRollupError(_command, o, helper, rollupUpdatedColumn);
    });

    let dependentFormulaColumns = column.fromFormulaColumn.get();
    dependentFormulaColumns = dependentFormulaColumns.filter(col => !removedColumns.includes(col.uid!));
    _.forEach(dependentFormulaColumns, dependentFormulaColumn => {
      const table = dependentFormulaColumn.fromTable.get()[0];
      const core = table.fromCore.get()[0];
      const workspace = core.fromWorkspace.get()[0];
      const _command = plainToClass(UpdateColumn_v2, {
        workspaceId: workspace.uid!,
        coreId: core.uid!,
        tableId: table.uid!,
        // @ts-ignore
        column: null,
        columnConfig: {
          type: dependentFormulaColumn.columnType,
          name: dependentFormulaColumn.name,
          formula: dependentFormulaColumn.formula,
          errorCode: ColumnErrorCodes.FORMULA_COLUMN_INVALID,
        },
        _metadata: command._metadata,
      });
      _command.column = dependentFormulaColumn;
      this.instanceFactory
        .getColumnInstance(dependentFormulaColumn.columnType)
        .updateColumn(_command, o, { columnCopy: dependentFormulaColumn });
      this.reportLookupOrRollupError(_command, o, helper, dependentFormulaColumn);
    });
    return this;
  }

  public getResultType(column: Column, dependentColumns: any[], isRollup = false): FormulaResultType {
    const { formula } = column;

    FormulaExecutor.checkFormula(formula, true);

    const { operators, functions, tokens } = FormulaExecutor.parseFormula(formula);

    // ignore the priority caused by parentheses temporarily
    for (const op of operators) {
      if (SUPPORTED_NUMBER_OPERATORS.includes(op)) {
        return FormulaResultType.NUMBER;
      }
    }

    // no operators
    if (!operators.length) {
      if (tokens.length) {
        const token = tokens[0];
        if (typeof token === 'number') {
          return FormulaResultType.NUMBER;
        } else if (token.startsWith('$')) {
          const dependentColumnId = token.replace('$column_value_', '');
          const dependentColumn = dependentColumns.find(c => c.uid === dependentColumnId);
          if (dependentColumn) {
            if (isRollup) {
              if (SUPPORTED_DATE_COLUMNS.includes(dependentColumn.columnType)) {
                return FormulaResultType.DATE;
              } else if (SUPPORTED_NUMBER_COLUMNS.includes(dependentColumn.columnType)) {
                return FormulaResultType.NUMBER;
              } else {
                return FormulaResultType.TEXT;
              }
            }
            if (dependentColumn.resultType) {
              // @ts-ignore
              return dependentColumn.resultType;
            } else {
              if (SUPPORTED_NUMBER_COLUMNS.includes(dependentColumn.columnType)) {
                return FormulaResultType.NUMBER;
              } else if (SUPPORTED_DATE_COLUMNS.includes(dependentColumn.columnType)) {
                return FormulaResultType.DATE;
              }
            }
          }
        }
      } else if (functions.length) {
        // MIN() and MAX() will return datetime or number according to dependent column
        if (functions[0] === 'MIN' || (functions[0] === 'MAX' && dependentColumns[0]?.columnType === 'DATETIME')) {
          return FormulaResultType.DATE;
        } else if (functions[0] === 'FROM_NOW') {
          // 判断FROM_NOW是否传进来timeUnit
          let argumentString: string = formula.substring(formula.indexOf('(') + 1, formula.lastIndexOf(')'));
          argumentString = argumentString?.replace(/\ /g, '');
          if (argumentString?.split(',').length === 1) {
            return FormulaResultType.TEXT;
          }
        }
        if (SUPPORTED_NUMBER_FUNCTIONS.includes(functions[0])) {
          return FormulaResultType.NUMBER;
        } else if (SUPPORTED_DATE_FUNCTIONS.includes(functions[0])) {
          return FormulaResultType.DATE;
        }
      }
    }

    return FormulaResultType.TEXT;
  }

  protected isSelfReference(symmetricColumn: Column, lookupConfig: LookupColumnConfig) {
    const { foreignLookupColumnId } = lookupConfig;
    // This method checks if the record reference column is referencing itself.
    if (!symmetricColumn || symmetricColumn.uid === foreignLookupColumnId) {
      throw new CircularReferenceError();
    }
  }

  protected async checkCircularReference(tableId: string, foreignLookupColumnId: string) {
    /* This method prevents the user from looking up a column that the lookup is from
    We could support this in the future but currently the recursive updates gets too crazy
    and we currently see no use case for why the user would want to lookup a column in the table
    that the lookup column is in. */
    const foreignReferenceColumn: any[] = await this.helperReadService.getEdges(
      foreignLookupColumnId,
      Relation.HAS_SYMMETRIC_COLUMN,
      [fromTableRelation()],
    );
    if (foreignReferenceColumn.length) {
      const rfTableId = _.get(foreignReferenceColumn, '[0].table[0].id');
      if (rfTableId === tableId) {
        throw new CircularReferenceError();
      }
    }
    const foreignLookupColumn: any[] = await this.helperReadService.getEdges(
      foreignLookupColumnId,
      Relation.HAS_FOREIGN_LOOKUP_COLUMN,
      [fromTableRelation()],
    );
    if (!foreignLookupColumn.length) {
      return;
    }

    const exist = foreignLookupColumn[0].table[0].id === tableId;
    if (exist) {
      throw new CircularReferenceError();
    }
    // Recursively check again if the foreign lookup column is another lookup column.
    await this.checkCircularReference(tableId, foreignLookupColumn[0]?.uid);
  }

  protected getNodeId(id: string) {
    return id.substr(0, 2) === '_:' ? id : `<${id}>`;
  }

  protected getNodeTempId(node: any) {
    return node.id || node.uid;
  }

  async getEdges(recordReferenceColumnId: string, relation: Relation) {
    return await this.helperReadService.getEdges(recordReferenceColumnId, relation);
  }

  async getColumnType(columnIds: string[], attributes: string[]) {
    return await this.helperReadService.getNodeByIds(columnIds, attributes);
  }

